#!/usr/bin/python

# Copyright (c) 2016, Ryan Scott Brown <ryansb@redhat.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
module: serverless
short_description: Manages a Serverless Framework project
description:
  - Provides support for managing Serverless Framework (U(https://serverless.com/)) project deployments and stacks.
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: none
  diff_mode:
    support: none
options:
  state:
    description:
      - Goal state of given stage/project.
    type: str
    choices: [absent, present]
    default: present
  serverless_bin_path:
    description:
      - The path of a serverless framework binary relative to the O(service_path), for example V(node_module/.bin/serverless).
    type: path
  service_path:
    description:
      - The path to the root of the Serverless Service to be operated on.
    type: path
    required: true
  stage:
    description:
      - The name of the serverless framework project stage to deploy to.
      - This uses the serverless framework default "dev".
    type: str
    default: ''
  region:
    description:
      - AWS region to deploy the service to.
      - This parameter defaults to V(us-east-1).
    type: str
    default: ''
  deploy:
    description:
      - Whether or not to deploy artifacts after building them.
      - When this option is V(false) all the functions are built, but no stack update is run to send them out.
      - This is mostly useful for generating artifacts to be stored/deployed elsewhere.
    type: bool
    default: true
  force:
    description:
      - Whether or not to force full deployment, equivalent to serverless C(--force) option.
    type: bool
    default: false
  verbose:
    description:
      - Shows all stack events during deployment, and display any Stack Output.
    type: bool
    default: false
notes:
  - Currently, the C(serverless) command must be in the path of the node executing the task. In the future this may be a flag.
requirements:
  - serverless
  - PyYAML
author:
  - Ryan Scott Brown (@ryansb)
"""

EXAMPLES = r"""
- name: Basic deploy of a service
  community.general.serverless:
    service_path: '{{ project_dir }}'
    state: present

- name: Deploy a project, then pull its resource list back into Ansible
  community.general.serverless:
    stage: dev
    region: us-east-1
    service_path: '{{ project_dir }}'
  register: sls

# The cloudformation stack is always named the same as the full service, so the
# cloudformation_info module can get a full list of the stack resources, as
# well as stack events and outputs
- cloudformation_info:
    region: us-east-1
    stack_name: '{{ sls.service_name }}'
    stack_resources: true

- name: Deploy a project using a locally installed serverless binary
  community.general.serverless:
    stage: dev
    region: us-east-1
    service_path: '{{ project_dir }}'
    serverless_bin_path: node_modules/.bin/serverless
"""

RETURN = r"""
service_name:
  type: str
  description: The service name specified in the serverless.yml that was just deployed.
  returned: always
  sample: my-fancy-service-dev
state:
  type: str
  description: Whether the stack for the serverless project is present/absent.
  returned: always
command:
  type: str
  description: Full C(serverless) command run by this module, in case you want to re-run the command outside the module.
  returned: always
  sample: serverless deploy --stage production
"""

import os

try:
    import yaml

    HAS_YAML = True
except ImportError:
    HAS_YAML = False

from ansible.module_utils.basic import AnsibleModule


def read_serverless_config(module):
    path = module.params.get("service_path")
    full_path = os.path.join(path, "serverless.yml")

    try:
        with open(full_path) as sls_config:
            config = yaml.safe_load(sls_config.read())
            return config
    except IOError as e:
        module.fail_json(msg=f"Could not open serverless.yml in {full_path}. err: {e}")


def get_service_name(module, stage):
    config = read_serverless_config(module)
    if config.get("service") is None:
        module.fail_json(msg="Could not read `service` key from serverless.yml file")

    if stage:
        return f"{config['service']}-{stage}"

    return f"{config['service']}-{config.get('stage', 'dev')}"


def main():
    module = AnsibleModule(
        argument_spec=dict(
            service_path=dict(type="path", required=True),
            state=dict(type="str", default="present", choices=["absent", "present"]),
            region=dict(type="str", default=""),
            stage=dict(type="str", default=""),
            deploy=dict(type="bool", default=True),
            serverless_bin_path=dict(type="path"),
            force=dict(type="bool", default=False),
            verbose=dict(type="bool", default=False),
        ),
    )

    if not HAS_YAML:
        module.fail_json(msg="yaml is required for this module")

    service_path = module.params.get("service_path")
    state = module.params.get("state")
    region = module.params.get("region")
    stage = module.params.get("stage")
    deploy = module.params.get("deploy", True)
    force = module.params.get("force", False)
    verbose = module.params.get("verbose", False)
    serverless_bin_path = module.params.get("serverless_bin_path")

    if serverless_bin_path is not None:
        command = f"{serverless_bin_path} "
    else:
        command = f"{module.get_bin_path('serverless')} "

    if state == "present":
        command += "deploy "
    elif state == "absent":
        command += "remove "
    else:
        module.fail_json(msg=f"State must either be 'present' or 'absent'. Received: {state}")

    if state == "present":
        if not deploy:
            command += "--noDeploy "
        elif force:
            command += "--force "

    if region:
        command += f"--region {region} "
    if stage:
        command += f"--stage {stage} "
    if verbose:
        command += "--verbose "

    rc, out, err = module.run_command(command, cwd=service_path)
    if rc != 0:
        if state == "absent" and f"-{stage}' does not exist" in out:
            module.exit_json(
                changed=False, state="absent", command=command, out=out, service_name=get_service_name(module, stage)
            )

        module.fail_json(msg=f"Failure when executing Serverless command. Exited {rc}.\nstdout: {out}\nstderr: {err}")

    # gather some facts about the deployment
    module.exit_json(
        changed=True, state="present", out=out, command=command, service_name=get_service_name(module, stage)
    )


if __name__ == "__main__":
    main()
